﻿using System.Collections;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Data;
using System.Diagnostics;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Text;
using Swimj.Core.Extensions;

namespace Swimj.Core.Utilities
{
    public static class ReflectionHelper
    {
        private static readonly ConcurrentDictionary<Type, IEnumerable<Type>> interfaceTypeCache =
            new ConcurrentDictionary<Type, IEnumerable<Type>>();

        public const BindingFlags PublicBindingFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance |
                                                       BindingFlags.Public | BindingFlags.GetField |
                                                       BindingFlags.GetProperty;

        public static TInterface? CreateInstanceFromInterfaceOrAbstractType<TInterface>(bool coverUpAbstractMembers)
            where TInterface : class
        {
            return CreateInstanceFromInterfaceOrAbstractType(typeof(TInterface), coverUpAbstractMembers) as TInterface;
        }

        public static T CreateInstance<T>()
        {
            return CreateInstance<T>(true, false);
        }

        public static T CreateInstance<T>(bool allowInterfacesAndAbstractClasses,
            bool coverUpAbstractMembers)
        {
            return (T)CreateInstance(typeof(T), allowInterfacesAndAbstractClasses, coverUpAbstractMembers);
        }

        public static object? GetDefaultValue(Type t)
        {
            if (t.IsValueType)
            {
                return Activator.CreateInstance(t);
            }

            return null;
        }

        public static PropertyInfo[] GetPropertiesRecursive(this Type type, BindingFlags flags,
            Func<Type, bool>? continueCondition = null)
        {
            if (continueCondition == null)
                continueCondition = t => t != typeof(object);

            var infos = new List<PropertyInfo>();
            Type typeToSearch = type;
            do
            {
                infos.AddRange(typeToSearch.GetProperties(flags));
                typeToSearch = typeToSearch.BaseType!;
            } while (continueCondition(typeToSearch) && typeToSearch != null);

            return infos.ToArray();
        }

        public static FieldInfo[] GetFieldsRecursive(this Type type, BindingFlags flags,
            Func<Type, bool>? continueCondition = null)
        {
            continueCondition ??= t => t != typeof(object);

            var infos = new List<FieldInfo>();
            var typeToSearch = type;
            do
            {
                infos.AddRange(typeToSearch.GetFields(flags));
                typeToSearch = typeToSearch.BaseType!;
            } while (continueCondition(typeToSearch) && typeToSearch != null);

            return infos.ToArray();
        }

        public static object CreateInstance(Type t, bool allowInterfacesAndAbstractClasses,
            bool coverUpAbstractMembers, bool tryResolve, IServiceProvider? serviceProvider = null)
        {
            object? result = null;
            if (tryResolve)
            {
                try
                {
                    result = (serviceProvider ?? new ServiceContainer()).GetService(t);
                }
                catch
                {
                    result = null;
                }
            }

            return result ?? (CreateInstance(t, allowInterfacesAndAbstractClasses, coverUpAbstractMembers));
        }

        public static object CreateInstance(Type t)
        {
            return CreateInstance(t, true, false);
        }

        public static object CreateInstance(Type t, bool allowInterfacesAndAbstractClasses,
            bool coverUpAbstractMembers)
        {
            if (typeof(Guid) == t)
                return Guid.NewGuid();
            object? result = null;
            try
            {
                if (allowInterfacesAndAbstractClasses && (t.IsInterface || t.IsArray || t.IsAbstract))
                {
                    result = CreateInstanceFromInterfaceOrAbstractType(t, coverUpAbstractMembers);
                }
                else if (t.GetConstructors().Any(info => !info.GetParameters().Any()))
                {
                    result = Activator.CreateInstance(t);
                }
            }
            catch (Exception)
            {
                result = null;
            }

            if (result == null)
            {
                List<object> parameters = new List<object>();
                var constructorInfo = t.GetConstructors().OrderBy(info => info.GetParameters().Count()).FirstOrDefault();
                if (constructorInfo != null)
                {
                    try
                    {
                        parameters.AddRange(constructorInfo.GetParameters()
                            .Select(parameterInfo => CreateInstance(parameterInfo.ParameterType)));
                        result = constructorInfo.Invoke(parameters.ToArray());
                    }
                    catch
                    {
                    }
                }
            }

            return result!;
        }

        public static object CreateGenericList(Type listType, Type entityType)
        {
            if (listType == null || entityType == null || !listType.IsGenericType)
            {
                return null;
            }
            Type type = listType.MakeGenericType(entityType);
            return CreateInstance(type);
        }

        public static Type? FindImplementingType(Type interfaceType)
        {
            IEnumerable<Type> additionalTypesToCheck = new List<Type>
        {
            typeof(List<>),
            typeof(Collection<>),
            typeof(HashSet<>),
            typeof(Dictionary<,>),
            typeof(Comparer<>),
            typeof(Queue<>),
            typeof(Stack<>),
        };

            var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
            Func<Type, IEnumerable<Type>> func = it => (from assembly in assemblies
                                                        where assembly.GetName().Version > new Version(0, 0, 0, 0)
                                                        from referencedAssembly in assembly.GetReferencedAssemblies()
                                                        where assembly == it.Assembly || referencedAssembly.FullName == it.Assembly.GetName().FullName
                                                        select assembly).SelectMany(assembly => assembly.GetTypes()).Concat(additionalTypesToCheck)
                .Where(type => type.ImplementsInterface(it))
                .ToList();

            IEnumerable<Type> implementingTypes = interfaceTypeCache.GetOrAdd(interfaceType, func).ToList();
            var result = implementingTypes.FirstOrDefault(
                             type => type.Name.Equals(interfaceType.Name.Substring(1),
                                 StringComparison.InvariantCultureIgnoreCase))
                         ?? implementingTypes.FirstOrDefault();

            result = TryMakeGenericIfNeeded(interfaceType, result);
            return result;
        }

        private static Type? TryMakeGenericIfNeeded(Type interfaceType, Type? result)
        {
            try
            {
                if (interfaceType.IsGenericType && interfaceType.GenericTypeArguments.Any() && result != null &&
                    result.IsGenericType)
                {
                    result = result.MakeGenericType(interfaceType.GenericTypeArguments);
                }
            }
            catch
            {
            }

            return result;
        }

        public static object? CreateInstanceFromInterfaceOrAbstractType(Type interfaceType, bool coverUpAbstractMembers)
        {
            if (typeof(IEnumerable).IsAssignableFrom(interfaceType))
            {
                if (interfaceType == typeof(IEnumerable) || (interfaceType.IsGenericType &&
                                                             interfaceType.GetGenericTypeDefinition() ==
                                                             typeof(IEnumerable<>)) ||
                    (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) ||
                    (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) ||
                    interfaceType.IsArray)
                {
                    return CreateList(interfaceType);
                }
            }

            if (!interfaceType.IsInterface && !interfaceType.IsAbstract)
                throw new NotSupportedException("Only interfaces are supported");

            var existingType = FindImplementingType(interfaceType);
            if (existingType != null)
            {
                return CreateInstance(existingType);
            }


            string? name = Assembly.GetCallingAssembly().GetName().Name;
            AssemblyName assemblyName = new AssemblyName { Name = name + ".Dynamic." + interfaceType.Name };


            AssemblyBuilder asmBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);

            var modBuilder = asmBuilder.DefineDynamicModule(asmBuilder.GetName().Name!);

            TypeBuilder typeBuilder = CreateTypeBuilder(modBuilder, "DynamicType_" + interfaceType.Name, interfaceType);
            if (interfaceType.IsInterface)
                typeBuilder.AddInterfaceImplementation(interfaceType);

            if (interfaceType.IsInterface || coverUpAbstractMembers)
            {
                foreach (var prop in interfaceType.GetProperties(PublicBindingFlags))
                {
                    BuildProperty(typeBuilder, prop.Name, prop.PropertyType);
                }

                foreach (var field in interfaceType.GetFields(PublicBindingFlags))
                {
                    BuildField(typeBuilder, field.Name, field.FieldType);
                }

                foreach (var method in interfaceType.GetMethods(PublicBindingFlags))
                {
                    BuildEmptyMethod(typeBuilder, method.Name, method.ReturnType);
                }
            }

            var type = typeBuilder.CreateTypeInfo()!;
            return Activator.CreateInstance(type);
        }

        public static Type?[] GetDeclaringTypes(Type t)
        {
            if (t.IsGenericType)
                return t.GetGenericArguments();
            return t.IsArray ? new[] { t.GetElementType() } : new[] { typeof(object) };
        }

        private static object? CreateList(Type interfaceType)
        {
            Type typeToCreate = typeof(ArrayList);
            if (interfaceType.IsGenericType || interfaceType.IsArray)
            {
                typeToCreate = typeof(List<>);
                if (typeof(IDictionary).IsAssignableFrom(interfaceType) ||
                    (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) ||
                    (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
                {
                    typeToCreate = typeof(Dictionary<,>);
                }

                typeToCreate = typeToCreate.MakeGenericType(GetDeclaringTypes(interfaceType)!);
            }

            return Activator.CreateInstance(typeToCreate);
        }

        #region Private Helper for typebuilder

        private static void BuildEmptyMethod(TypeBuilder typeBuilder, string name, Type type)
        {
            const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.HideBySig |
                                                MethodAttributes.SpecialName |
                                                MethodAttributes.Virtual;

            MethodBuilder getter = typeBuilder.DefineMethod(name, getSetAttr, type, Type.EmptyTypes);

            ILGenerator getIL = getter.GetILGenerator();
            getIL.Emit(OpCodes.Ldarg_0);
            getIL.Emit(OpCodes.Ret);
        }

        private static void BuildField(TypeBuilder typeBuilder, string name, Type type)
        {
            typeBuilder.DefineField(name, type, FieldAttributes.Public);
        }

        private static void BuildProperty(TypeBuilder typeBuilder, string name, Type type)
        {
            FieldBuilder field = typeBuilder.DefineField("m" + name, type, FieldAttributes.Private);
            PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(name, PropertyAttributes.None, type, null);

            const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.HideBySig |
                                                MethodAttributes.SpecialName |
                                                MethodAttributes.Virtual;

            MethodBuilder getter = typeBuilder.DefineMethod("get_" + name, getSetAttr, type, Type.EmptyTypes);

            ILGenerator getIL = getter.GetILGenerator();
            getIL.Emit(OpCodes.Ldarg_0);
            getIL.Emit(OpCodes.Ldfld, field);
            getIL.Emit(OpCodes.Ret);

            MethodBuilder setter = typeBuilder.DefineMethod("set_" + name, getSetAttr, null, new[] { type });

            ILGenerator setIL = setter.GetILGenerator();
            setIL.Emit(OpCodes.Ldarg_0);
            setIL.Emit(OpCodes.Ldarg_1);
            setIL.Emit(OpCodes.Stfld, field);
            setIL.Emit(OpCodes.Ret);


            propertyBuilder.SetGetMethod(getter);
            propertyBuilder.SetSetMethod(setter);
        }


        private static TypeBuilder CreateTypeBuilder(ModuleBuilder modBuilder, string typeName, Type interfaceType)
        {
            Type parentType = typeof(object);
            if (!interfaceType.IsInterface)
                parentType = interfaceType;
            TypeBuilder typeBuilder = modBuilder.DefineType(typeName,
                TypeAttributes.Public |
                TypeAttributes.Class |
                TypeAttributes.AutoClass |
                TypeAttributes.AnsiClass |
                TypeAttributes.BeforeFieldInit |
                TypeAttributes.AutoLayout,
                parentType, interfaceType.IsInterface ? new[] { interfaceType } : new Type[0]);

            return typeBuilder;
        }

        #endregion


        public static T SimpleCast<T>(object o)
        {
            var res = SimpleCast(o, typeof(T));
            if (res != null)
                return (T)res;
            return default(T)!;
        }

        public static object? SimpleCast(object o, Type targetType)
        {
            object? result;
            TrySimpleCast(o, targetType, out result);
            return result;
        }

        public static bool TrySimpleCast(object o, Type targetType, out object? result)
        {
            result = null;
            MethodInfo mi = o.GetType().GetMethods((BindingFlags.Public | BindingFlags.Static)).FirstOrDefault(info =>
                                (info.Name == "op_Implicit" || info.Name == "op_Explicit")
                                && targetType.IsAssignableFrom(info.ReturnType) && info.GetParameters().Count() == 1
                                && o.GetType().IsAssignableFrom(info.GetParameters()[0].ParameterType))
                            ??
                            targetType.GetMethods((BindingFlags.Public | BindingFlags.Static)).FirstOrDefault(info =>
                                (info.Name == "op_Implicit" || info.Name == "op_Explicit")
                                && targetType.IsAssignableFrom(info.ReturnType) && info.GetParameters().Count() == 1
                                && o.GetType().IsAssignableFrom(info.GetParameters()[0].ParameterType))!;

            if (mi != null)
            {
                var invoke = mi.Invoke(null, new[] { o });
                result = invoke;
                return true;
            }

            if (targetType.IsInstanceOfType(o))
            {
                var typeConverter = TypeDescriptor.GetConverter(targetType);
                if (typeConverter.CanConvertTo(targetType))
                {
                    result = typeConverter.ConvertTo(o, targetType);
                    return true;
                }

                typeConverter = TypeDescriptor.GetConverter(o.GetType());
                if (typeConverter.CanConvertFrom(o.GetType()))
                {
                    result = typeConverter.ConvertFrom(o);
                    return true;
                }
            }

            return false;
        }

        public static IDictionary<string, object> ToDictionary(this Type type,
            Func<Type, MemberInfo, bool>? condition = null)
        {
            var flags = PublicBindingFlags | BindingFlags.Static;

            condition ??= (t, f) => true;
            var result = new Dictionary<string, object>();

            result.AddRange(type.GetFields(flags)
                .Where(fi => condition(fi.FieldType, fi))
                .ToDictionary(fi => fi.Name, fi => fi.GetValue(null))!);

            result.AddRange(type.GetProperties(flags)
                .Where(fi => condition(fi.PropertyType, fi))
                .ToDictionary(fi => fi.Name, fi => fi.GetValue(null))!);

            return result;
        }

        [DebuggerStepThrough]
        public static Dictionary<string, object> GetProperties(object objectValue, params string[] except)
        {
            var result = new Dictionary<string, object>();
            BindingFlags bindingInfo = BindingFlags.Public | BindingFlags.Static;
            Type? objectType = objectValue as Type;
            object? objectInstance = null;
            if (objectType == null)
            {
                bindingInfo = BindingFlags.Public | BindingFlags.Instance;
                objectType = objectValue.GetType();
                objectInstance = objectValue;
            }

            foreach (var propertyInfo in from info in objectType.GetProperties(bindingInfo)
                                         where !except.Contains(info.Name)
                                         select info)
            {
                try
                {
                    var value = propertyInfo.GetValue(objectInstance, new object[0])!;
                    result.Add(propertyInfo.Name, value);
                }
                catch
                {
                }
            }

            return result;
        }

        public static string GetSignature(this MethodInfo method, bool callable = false)
        {
            var firstParam = true;
            var sigBuilder = new StringBuilder();
            if (!callable)
            {
                if (method.IsPublic)
                    sigBuilder.Append("public ");
                else if (method.IsPrivate)
                    sigBuilder.Append("private ");
                else if (method.IsAssembly)
                    sigBuilder.Append("internal ");
                if (method.IsFamily)
                    sigBuilder.Append("protected ");
                if (method.IsStatic)
                    sigBuilder.Append("static ");
                sigBuilder.Append(TypeName(method.ReturnType));
                sigBuilder.Append(' ');
            }

            sigBuilder.Append(method.Name);

            if (method.IsGenericMethod)
            {
                sigBuilder.Append("<");
                foreach (var g in method.GetGenericArguments())
                {
                    if (firstParam)
                        firstParam = false;
                    else
                        sigBuilder.Append(", ");
                    sigBuilder.Append(TypeName(g));
                }

                sigBuilder.Append(">");
            }

            sigBuilder.Append("(");
            firstParam = true;
            var secondParam = false;
            foreach (var param in method.GetParameters())
            {
                if (firstParam)
                {
                    firstParam = false;
                    if (method.IsDefined(typeof(ExtensionAttribute), false))
                    {
                        if (callable)
                        {
                            secondParam = true;
                            continue;
                        }

                        sigBuilder.Append("this ");
                    }
                }
                else if (secondParam)
                    secondParam = false;
                else
                    sigBuilder.Append(", ");

                if (param.ParameterType.IsByRef)
                    sigBuilder.Append("ref ");
                else if (param.IsOut)
                    sigBuilder.Append("out ");
                if (!callable)
                {
                    sigBuilder.Append(TypeName(param.ParameterType));
                    sigBuilder.Append(' ');
                }

                sigBuilder.Append(param.Name);
            }

            sigBuilder.Append(")");
            return sigBuilder.ToString();
        }

        public static bool ImplementsInterface(this Type type, Type interfaceType)
        {
            type = TryMakeGenericIfNeeded(interfaceType, type)!;
            Type[] array = type.FindInterfaces((typeObj, criteriaObj) => typeObj == (Type)criteriaObj!, interfaceType);
            return array.Length > 0; // || interfaceType.IsAssignableFrom(type);
        }

        public static string TypeName(Type type)
        {
            var nullableType = Nullable.GetUnderlyingType(type);
            if (nullableType != null)
                return nullableType.Name + "?";

            if (!type.IsGenericType)
                switch (type.Name)
                {
                    case "String": return "string";
                    case "Int32": return "int";
                    case "Decimal": return "decimal";
                    case "Object": return "object";
                    case "Void": return "void";
                    default:
                        {
                            return String.IsNullOrWhiteSpace(type.FullName) ? type.Name : type.FullName;
                        }
                }

            var sb = new StringBuilder(type.Name.Substring(0,
                type.Name.IndexOf('`'))
            );
            sb.Append('<');
            var first = true;
            foreach (var t in type.GetGenericArguments())
            {
                if (!first)
                    sb.Append(',');
                sb.Append(TypeName(t));
                first = false;
            }

            sb.Append('>');
            return sb.ToString();
        }


        public static object? GetValue(object obj, string property)
        {
            var dataRowView = obj as DataRowView;
            if (dataRowView != null)
            {
                return dataRowView[property];
            }

            if (obj == null)
                return null;
            if (String.IsNullOrEmpty(property))
                return obj;
            var propertyInfo = obj.GetType().GetProperty(property);
            if (propertyInfo == null)
                return obj;
            var result = propertyInfo.GetValue(obj, new object[] { });
            return result ?? obj;
        }

        public static MethodBase? GetCallingMethod(int skip = 0)
        {
            var st = new StackTrace(2 + skip, true);

            try
            {
                StackFrame? sf = st.GetFrame(0);
                if (sf != null)
                    return sf?.GetMethod();
            }
            catch (Exception)
            {
                return MethodBase.GetCurrentMethod();
            }

            return MethodBase.GetCurrentMethod();
        }


        public static bool IsSystemType(object obj)
        {
            if (obj == null)
            {
                return false;
            }
            return IsSystemType(obj.GetType());
        }

        public static bool IsSystemType(Type type)
        {
            return type.FullName.StartsWith("System");
        }

        public static bool IsList(object obj)
        {
            if (obj != null)
            {
                return IsList(obj.GetType());
            }
            return false;
        }

        public static bool IsList(Type type)
        {
            if (type == null)
            {
                return false;
            }
            return typeof(IList).IsAssignableFrom(type);
        }

        public static bool IsCollection(object obj)
        {
            if (obj != null)
            {
                return IsCollection(obj.GetType());
            }
            return false;
        }

        public static bool IsCollection(Type type)
        {
            if (type == null)
            {
                return false;
            }
            return typeof(ICollection).IsAssignableFrom(type);
        }

        public static bool IsEnumerable(object obj)
        {
            if (obj != null)
            {
                return IsEnumerable(obj.GetType());
            }
            return false;
        }

        public static bool IsEnumerable(Type type)
        {
            if (type == null)
            {
                return false;
            }
            return typeof(IEnumerable).IsAssignableFrom(type);
        }

        public static bool IsDictionary(object obj)
        {
            if (obj != null)
            {
                return IsDictionary(obj.GetType());
            }
            return false;
        }

        public static bool IsDictionary(Type type)
        {
            if (type == null)
            {
                return false;
            }
            return typeof(IDictionary).IsAssignableFrom(type);
        }

        public static bool IsGenericList(object obj)
        {
            if (obj != null)
            {
                return IsGenericList(obj.GetType());
            }
            return false;
        }

        public static bool IsGenericList(Type type)
        {
            if (IsList(type))
            {
                return type.IsGenericType;
            }
            return false;
        }

        public static bool IsGenericDictionary(object obj)
        {
            if (obj != null)
            {
                return IsGenericDictionary(obj.GetType());
            }
            return false;
        }

        public static bool IsGenericDictionary(Type type)
        {
            if (IsDictionary(type))
            {
                return type.IsGenericType;
            }
            return false;
        }

        public static Type GetListGenericType(object listObject)
        {
            if (!IsGenericList(listObject))
            {
                return null;
            }
            return listObject.GetType().GetGenericArguments()[0];
        }

        public static Type[] GetDictionaryGenericType(object dictionaryObject)
        {
            if (!IsDictionary(dictionaryObject))
            {
                return null;
            }
            return dictionaryObject.GetType().GetGenericArguments();
        }

        public static Type GetType(string typeName)
        {
            if (string.IsNullOrEmpty(typeName))
            {
                return null;
            }
            Type type = null;
            string[] array = typeName.Split(',');
            if (array.Length == 2)
            {
                Assembly assembly = Assembly.Load(array[1]);
                type = assembly.GetType(array[0]);
            }
            else
            {
                Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
                Assembly[] array2 = assemblies;
                foreach (Assembly assembly2 in array2)
                {
                    try
                    {
                        type = assembly2.GetType(typeName);
                        if (!(type != null))
                        {
                            continue;
                        }
                        return type;
                    }
                    catch
                    {
                    }
                }
            }
            return type;
        }

        public static MemberInfo[] GetMembers(string typeName)
        {
            return GetMembers(GetType(typeName));
        }

        public static MemberInfo[] GetMembers(Type type)
        {
            if (type == null)
            {
                return new MemberInfo[0];
            }
            return type.GetMembers(PublicBindingFlags);
        }

        public static MemberInfo GetMember(Type target, string memberName)
        {
            MemberInfo[] members = GetMembers(target);
            if (members == null || members.Length <= 0)
            {
                return null;
            }
            return members.Where((MemberInfo m) => string.Equals(m.Name, memberName)).FirstOrDefault();
        }

        public static Type GetMemberType(Type target, string memberName)
        {
            MemberInfo member = GetMember(target, memberName);
            Type result = null;
            if (member != null)
            {
                if (member.MemberType == MemberTypes.Property)
                {
                    result = ((PropertyInfo)member).PropertyType;
                }
                else if (member.MemberType == MemberTypes.Field)
                {
                    result = ((FieldInfo)member).FieldType;
                }
                else if (member.MemberType == MemberTypes.Method)
                {
                    result = ((MethodInfo)member).ReturnType;
                }
            }
            return result;
        }

    }
}
